#!/usr/bin/python3 -u

import sys
import os
import locale
import gettext
import hashlib
import re
import subprocess
from systemd import journal

GETTEXT_PROGNAME = "abrt"

_ = gettext.gettext

def file_has_string(filename, string):
    try:
        with open(filename, "r") as f:
            for line in f:
                if string in line:
                    return True
    except OSError as ex:
        sys.stderr.write("Failed to read file '%s': %s\n"
                         % (filename, str(ex)))
    except ValueError as ex:
        sys.stderr.write("Invalid data in file '%s': %s\n"
                         % (filename, str(ex)))
    return False


def tail_with_search(filename, string, maxlen):
    # return empty list on error
    retval = []
    try:
        with open(filename, "r") as f:
            l = []
            for line in f:
                if string in line:
                    l.append(line)
                    if len(l) > maxlen:
                        del l[0]
            retval = l
    except OSError as ex:
        sys.stderr.write("Failed to read file '%s': %s\n"
                         % (filename, str(ex)))
    except ValueError as ex:
        sys.stderr.write("Invalid data in file '%s': %s\n"
                         % (filename, str(ex)))
    return retval


def open_or_die(filename, mode):
    try:
        f = open(filename, mode)
    except IOError as e:
        sys.stderr.write(str(e) + "\n")
        sys.exit(1)
    return f


def journal_has_string(identifier, string):
    j = journal.Reader()
    j.this_boot()
    j.this_machine()
    j.log_level(journal.LOG_INFO)
    j.add_match(SYSLOG_IDENTIFIER=identifier, MESSAGE=string)

    return bool(list(j))


def tail_journal_with_search(identifier, maxlen):
    args = ['journalctl', '-b', '-n', str(maxlen), '-t', str(identifier)]
    logs = subprocess.check_output(args, stderr=subprocess.STDOUT, universal_newlines=True)
    return logs


if __name__ == "__main__":
    try:
        locale.setlocale(locale.LC_ALL, "")
    except locale.Error:
        os.environ['LC_ALL'] = 'C'
        locale.setlocale(locale.LC_ALL, "")

    gettext.bindtextdomain(GETTEXT_PROGNAME, '/usr/share/locale')
    gettext.textdomain(GETTEXT_PROGNAME)

    #
    # So far we only look for Machine Check Exceptions here.
    #

    # See if MCEs were seen
    oops_mce = file_has_string("backtrace", "Machine check events logged");
    vmcore_mce = file_has_string("backtrace", "Machine Check Exception:");
    if not oops_mce and not vmcore_mce:
        sys.exit(0)
    #
    # There was an MCE. IOW: it's not a bug, it's a HW error.
    f = open_or_die("not-reportable", "w")
    f.write(_(
                "The kernel log indicates that hardware errors were detected.\n"
                "This is most likely not a software problem.\n"
    ))
    f.close()

    oops_hash = hashlib.sha1()
    with open("backtrace", "r") as btfile:
        for line in btfile:
            oops_hash.update(line.encode())

    with open_or_die("uuid", "w") as f:
        f.write(oops_hash.hexdigest())

    with open_or_die("duphash", "w") as f:
        f.write(oops_hash.hexdigest())

    res = tail_with_search("dmesg", "Linux version", 1)
    if res:
        kernel = re.sub(r"^.*Linux version ([^ ]+) .*$", r"\1", res[0]);
        with open_or_die("kernel", "w") as krnlfile:
            krnlfile.write(kernel)

    with open_or_die("mce", "w") as f:
        f.write("fatal" if vmcore_mce else "non-fatal")

    # vmcore MCEs already have good backtrace element, nothing more to do
    if vmcore_mce:
        sys.exit(0)

    #
    # Did mcelog logged it to /var/log/mcelog
    # (RHEL6 by default does this)?
    if os.path.exists("/var/log/mcelog"):
        f = open_or_die("backtrace", "w")
        f.write("The kernel log indicates that hardware errors were detected.\n")
        f.write("/var/log/mcelog file may have more information.\n")
        f.write("The last 20 lines of /var/log/mcelog are:\n")
        f.write("=========================================\n")
        #tail -n20 /var/log/mcelog 2>&1
        l = tail_with_search("/var/log/mcelog", "", 20)
        for line in l:
            f.write(line)
        f.close()
        sys.exit(0)
    #
    # Check systemd-journal for MCEs.
    #
    if journal_has_string("mcelog", "Hardware event. This is not a software error."):
        f = open_or_die("backtrace", "w")
        f.write("The kernel log indicates that hardware errors were detected.\n")
        f.write("System log may have more information.\n")
        f.write("The last 20 mcelog lines of system log are:\n")
        f.write("==========================================\n")
        # print last 20 journal messages
        # journalctl -b -n20 -t mcelog
        l = tail_journal_with_search('mcelog', 20)
        for line in l:
            f.write(line)
        f.close()
        sys.exit(0)
    #
    # On RHEL7, mcelog is run so that its output ends up in syslog.
    # Do we see that?
    elif file_has_string("/var/log/messages", "mcelog: Hardware event"):
        f = open_or_die("backtrace", "w")
        f.write("The kernel log indicates that hardware errors were detected.\n")
        f.write("System log may have more information.\n")
        f.write("The last 20 mcelog lines of system log are:\n")
        f.write("==========================================\n")
        #grep -Fi 'mcelog:' /var/log/messages | tail -n20 2>&1
        l = tail_with_search("/var/log/messages", "mcelog:", 20)
        for line in l:
            f.write(line)
        f.close()
        sys.exit(0)
    #
    # Apparently, there is no running mcelog daemon!
    # Let user know that he needs one.
    f = open_or_die("backtrace", "w")
    f.write("The kernel log indicates that hardware errors were detected.\n")
    f.write("The data was saved by kernel for processing by the mcelog tool.\n")
    f.write("However, neither /var/log/mcelog nor system log contain mcelog messages.\n")
    f.write("Most likely reason is that mcelog is not installed or not configured\n")
    f.write("to be started during boot.\n")
    f.write("Without this tool running, the binary data saved by kernel\n")
    f.write("is of limited usefulness.\n")
    f.write("(You can save this data anyway by running 'cat </dev/mcelog >FILE').\n")
    f.write("The recommended course of action is to install mcelog.\n")
    f.write("If another hardware error would occur, a user-readable description\n")
    f.write("of it will be saved in system log or /var/log/mcelog.\n")
    f.close()
